namespace IBS.Math

[<AutoOpen>]
module TVM = 
    open System

    type Frequency =
        | Annually = 1
        | Semiannually = 2
        | Quarterly = 4
        | Monthly = 12
        | Daily = 365
        | None = 0

    let inline internal fvAnnuityFactor rate ps = 1. / rate * (pown (1.+rate) ps - 1.)
    let inline internal pvAnnuityFactor rate ps = 1. / rate * (1. - 1. / pown (1. + rate) ps)
    let inline internal nperFactor rate pmt v = v * rate + pmt

    /// future value for continuously compounded securities 
    let fvCont rate years pv = -pv * exp (rate * years)
    let pvCont rate years fv = -fv / exp (rate * years)

    let inline (|NoCompounding|_|) x = if x = Frequency.None then Some() else None

    /// future value
    // rate - the annualized interest rate
    // pmt - the payment made each period (at the end)
    let fv rate years pmt pv (compFreq: Frequency) =
        if rate < 0. then failwith "rate should be positive or zero"
        if years <= 0. then failwith "time period should be positive"
        if int compFreq < 0 then failwith "compounding frequency should be positive or zero"

        let p = years * float compFreq
        match compFreq, rate with
        | NoCompounding, _ | _, 0. -> -(pv * (1. + rate) + pmt) * p
        | _ ->
            let rt = rate / float compFreq
            let factor = fvAnnuityFactor rt (int <| floor p)
            - (pv * (1. + rt) ** p + pmt * factor)


    /// present value
    let pv rate years pmt fv (compFreq: Frequency) =
        if rate < 0. then failwith "rate should be positive or zero"
        if years <= 0. then failwith "time period should be positive"
        if int compFreq < 0 then failwith "compounding frequency should be positive or zero"

        let p = years * float compFreq
        match compFreq, rate with
        | NoCompounding, _ | _, 0. -> -(fv / (1. + rate) + pmt) / p
        | _ ->
            let rt = rate / float compFreq
            let factor = pvAnnuityFactor rt (int <| floor p)
            - (fv / (1. + rt) ** p + pmt * factor)

    /// calculate payment 
    let pmt rate nper pv fv (compFreq: Frequency) =
        if rate < 0. then failwith "rate should be positive or zero"
        if nper <= 0. then failwith "the number of periods should be positive"

        match rate with
        | 0. -> -(pv + fv) / nper
        | _ ->
            let r = rate / float compFreq
            let p = nper * float compFreq
            let factor = fvAnnuityFactor r (int <| floor p)
            - (fv + pv * (1. + r) ** p) / factor

    /// the number of periods for an investment based on periodic, constant payments and a constant interest rate
    let nper rate pmt pv fv =
        log (nperFactor rate pmt -fv / nperFactor rate pmt pv) / log (1. + rate)
        
    /// find periodic rate
    let rate nper pmt presentValue futureValue =
        let f r = pv r nper pmt futureValue Frequency.Annually - presentValue
        Solver.findRoot f 0.1

